自已项目中需要使用一个类此为 iTunes 中音乐评星的功能. 在 gihub 中找了一些发现都太繁琐. 本着一切从简, 能自己动手就自己动手的原则, 就参照开源的实现自己写了一个.

先来看看图片中我们要实现的控件.
1.01.45
这个 StarRatingView 有两种状态, 分别是可编辑和不可编辑的状态. UI结构是一个 baseView 中拥有 5 个 UIImageView, 根据用户手指的移动 CGPoint, 为相应的 UIImageView 填充高亮或普通的 star 图片.

首先, 我们创建一个 StarRatingView 的类, 它继承自 UIView.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class StarRatingView: UIView {
// 普通的 star 图片
private var notSelectedImage: UIImage = UIImage(named: "star_empty")! {
didSet {
refresh()
}
}
// 高亮的 star 图片
private var halfSeletedImage: UIImage = UIImage(named: "star_full")! {
didSet {
refresh()
}
}
// 是否可以编辑
@IBInspectable var editable: Bool = true
// 当前高亮 star 的数量.
@IBInspectable var rating: Int = 0 {
didSet {
refresh()
}
}
// 用来存储 StarRatingView 中所有的 星星 imageView
private var imageViews = [UIImageView]()
}

接着我们定义一个maxRating属性, 它用来表示最大星星数量, 默认值是 5. 在它的 didSet 方法中, 我们进行 UIImageView 的初始化, 创建相应个数的 UIImageView.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var maxRating: Int = 5 {
didSet {
imageViews.forEach { $0.removeFromSuperview() }
imageViews.removeAll()
for _ in 0..<maxRating {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageViews.append(imageView)
addSubview(imageView)
}
setNeedsLayout()
refresh()
}
}

之后我们来定义上面使用到的 refresh 方法, 它的逻辑非常简单, 就是遍历 imageViews 数组, 填充相应的图片.

1
2
3
4
5
6
7
8
9
10
11
private func refresh() {
imageViews.enumerated().forEach {
index, imageView in
if self.rating >= index + 1 {
imageView.image = fullSelectedImage
} else {
imageView.image = notSelectedImage
}
}
}

接着, 来处理 StarRatingView 的布局. 我们重写 layoutSubviews 的方法. 遍历 imageViews 数组, 为其设置计算好的 frame.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private var midMargin: CGFloat = 0
private var leftMargin: CGFloat = 0
private var minImageSize: CGSize = CGSize.zero
override func layoutSubviews() {
super.layoutSubviews()
var desiredImageWidth: CGFloat = 0
if editable {
desiredImageWidth = (self.frame.size.width - (self.leftMargin * 2) -
(self.midMargin * CGFloat(self.imageViews.count))) /
CGFloat(self.imageViews.count)
} else {
desiredImageWidth = self.frame.size.width / 5
}
let imageWidth = max(self.minImageSize.width, desiredImageWidth)
let imageHeight = max(self.minImageSize.height, self.frame.size.height)
imageViews.enumerated().forEach {
index, imageView in
imageView.frame = CGRect(x: self.leftMargin + CGFloat(index) *
(self.midMargin + imageWidth), y: 0, width: imageWidth, height:
imageHeight)
}
}

最后我们来处理用户手指的触摸动作. 我们定义一个 private 的方法 handleTouch(location: CGPoint) 它接受一个用户点击的坐标. 以此来计算需要显示多少个 star.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func handleTouch(location: CGPoint) {
if !self.editable { return }
var newRating = 0
for (index, imageView) in imageViews.enumerated().reversed() {
if location.x > imageView.frame.origin.x {
newRating = index + 1
break
}
}
self.rating = newRating
}

这样我们就可以在 UIView 的响应触摸事件的方法中调用 handleTouch(location: CGPoint) 了.

1
2
3
4
5
6
7
8
9
10
11
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: self)
handleTouch(location: location)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: self)
handleTouch(location: location)
}

OK! Done.

在外部使用的时候, 我们可以指定其最大的 star 数量, 也可以通过 ratring 属性来控制当前高亮的 star 数量, 你也可以非常方便的更换 star 图片.